学习契机
项目中使用Elasticsearch(ES)存储海量业务数据,基于ES向外提供的API进一层封装,按需处理原始数据提供更精确、更多样化的结果。在研究这一层的代码时接触到@six.add_metaclass(abc.ABCMeta)
,故而学习一下Python的元类。不过,虽然@six.add_metaclass(abc.ABCMeta)
实现上与元类有关,但实际应用只需要调用其接口,并不需要接触后幕后的元类操作。
翻译这篇答案是为了方便自己记忆理解,其实原文中一些地方我自己不是很明白,所以这个翻译会根据自己理解的程度持续更新。
原链接
stackoverflow-What are metaclasses in Python?
Python中的元类是什么
类也是对象
在理解元类之前,需要掌握Python中类概念。Python的类概念稍有奇特,其借鉴于Smalltalk。
在大部分语言中,类用于描述如何生成一个对象,在Python中也是如此:
>>> class ObjectCreator(object):
... pass
...
>>> my_object = ObjectCreator()
>>> print(my_object)
<__main__.ObjectCreator object at 0x8974f2c>
但是,在Python中类的意义更多,类同时也是对象
。当你使用关键字class时,Python解释器执行代码时会生成一个对象。以下代码会在内存中创建一个名为“ObjectCreator”的对象:
>>> class ObjectCreator(object):
... pass
...
这个对象(类)自身可以创建对象(实例),这是为什么它是类的原因。
不过它仍然是一个对象,你可以:
- 可以将它赋值给变量
- 可以复制
- 可以添加属性
TODO 添加属性只是对象的特性?
- 可以将其当作函数参数传递
举例:
>>> print(ObjectCreator) # you can print a class because it's an object
<class '__main__.ObjectCreator'>
>>> def echo(o):
... print(o)
...
>>> echo(ObjectCreator) # you can pass a class as a parameter
<class '__main__.ObjectCreator'>
>>> print(hasattr(ObjectCreator, 'new_attribute'))
False
>>> ObjectCreator.new_attribute = 'foo' # you can add attributes to a class
>>> print(hasattr(ObjectCreator, 'new_attribute'))
True
>>> print(ObjectCreator.new_attribute)
foo
>>> ObjectCreatorMirror = ObjectCreator # you can assign a class to a variable
>>> print(ObjectCreatorMirror.new_attribute)
foo
>>> print(ObjectCreatorMirror())
<__main__.ObjectCreator object at 0x8997b4c>
动态创建类
既然类也是对象,那就可像其他对象那样,动态创建类。
首先,可以使用关键字class,在函数中创建类:
>>> def choose_class(name):
... if name == 'foo':
... class Foo(object):
... pass
... return Foo # return the class, not an instance
... else:
... class Bar(object):
... pass
... return Bar
...
>>> MyClass = choose_class('foo')
>>> print(MyClass) # the function returns a class, not an instance
<class '__main__.Foo'>
>>> print(MyClass()) # you can create an object from this class
<__main__.Foo object at 0x89c6d4c>
但是这还不够动态,因为还是需要自己编写整个类的代码。所以,想一想,这些类既然是对象,就必然是某种东西生成的。当使用class关键字时,Python自动创建了类这个对象,但是像Python中大部分事情一样,Python中也可以手动创建类。
还记type方法吗?一个可以让你知道一个对象是什么类型的方法:
>>> print(type(1))
<type 'int'>
>>> print(type("1"))
<type 'str'>
>>> print(type(ObjectCreator))
<type 'type'>
>>> print(type(ObjectCreator()))
<class '__main__.ObjectCreator'>
除此之外,type还有一个完全不同的功能:动态创建类。将类的描述作为参数传递给type,会返回一个类。(我知道,同一个函数根据传参的不同而展示出两种完全不同的功能很不合理,不过这是为了Python的向后兼容。)
type如何创建类:
type(类名,
父类元祖 (可为空),
包含键值对属性的字典)
举例:
>>> class MyShinyClass(object):
... pass
上面这个MyShinyClass类可以用以下方法手动创建:
>>> MyShinyClass = type('MyShinyClass', (), {}) # 返回一个类
>>> print(MyShinyClass)
<class '__main__.MyShinyClass'>
>>> print(MyShinyClass()) # 创建一个类对象
<__main__.MyShinyClass object at 0x8997cec>
应该注意到了,使用"MyShinyClass"作为类名,也将其作为一个变量名,赋值为类的引用。类名和变量名是可以不同的,但是没必要把事情搞复杂。
type可以接受一个定义类属性的字典作为参数:
>>> class Foo(object):
... bar = True
以上定义等同于:
>>> Foo = type('Foo', (), {'bar':True})
使用起来跟一个普通类一样:
>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo.bar)
True
>>> f = Foo()
>>> print(f)
<__main__.Foo object at 0x8a9b84c>
>>> print(f.bar) # 利用实例打印类属性
True
当然,也可以作为基类,给其他类继承:
>>> class FooChild(Foo):
... pass
以上代码等同于:
>>> FooChild = type('FooChild', (Foo,), {})
>>> print(FooChild)
<class '__main__.FooChild'>
>>> print(FooChild.bar) # bar属性继承自类Foo
True
你肯定还想为类添加方法。只需要定义一个名称合理的函数,并将这个函数名作为属性传递给type就行:
>>> def echo_bar(self):
... print(self.bar)
...
>>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})
>>> hasattr(Foo, 'echo_bar')
False
>>> hasattr(FooChild, 'echo_bar')
True
>>> my_foo = FooChild()
>>> my_foo.echo_bar()
True
在动态创建一个类之后,为这个类添加更多的方法,就像为一个正常创建的类添加方法一样:
>>> def echo_bar_more(self):
... print('yet another method')
...
>>> FooChild.echo_bar_more = echo_bar_more
>>> hasattr(FooChild, 'echo_bar_more')
True
现在已经看到:在Python中,类也是对象,可以动态地创建类。当使用关键字class时,Python使用元类像这样创建类的。
什么是元类(终于讲到了)
元类就是创建类的“东西”。我们定义类是为了创建对象,是吧?但是我们认识到在Python中类也是对象,而元类就是创建类这种对象(类)的,它们是类的类,你可以这样理解:
MyClass = MetaClass()
MyObject = MyClass()
你已经看到type可以让你做如下操作:
MyClass = type('MyClass', (), {})
这是因为type方法实际上是一个元类。事实上,type是Python中用于创建所有类的元类。不过现在你一定很奇怪为什么这个类名首字母是小写,而不是Type?我猜这是同str保持一致,str是用来创建string对象的类,int是用来创建integer对象的类,type则是创建类对象的类。
在Python中,一切,对就是一切,都是对象。包括整数、字符串、函数和类。所有东西都是对象,它们都是创建自某个类。
通过__class__属性可以验证这一点:
>>> age = 35
>>> age.__class__
<type 'int'>
>>> name = 'bob'
>>> name.__class__
<type 'str'>
>>> def foo(): pass
>>> foo.__class__
<type 'function'>
>>> class Bar(object): pass
>>> b = Bar()
>>> b.__class__
<class '__main__.Bar'>
那么,一个__class__的__class__属性是什么呢?
>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>
由此可见:元类确实就是创建类对象的东西。如果你觉得合适你就可以称之为“类工厂”。type是Python使用的内置元类,当然,你也可以自己创建元类。
__metaclass__属性
编写一个类时添加上__metaclass__属性:
class Foo(object):
__metaclass__ = something...
[...]
如果你像上面这样做,Python就会使用元类创建一个Foo类。
要当心了,这里有些小圈套。你先写下了“class Foo(object)”,但此时内存中还没有创建Foo类对象。Python会在类的声明中寻找属性__metaclass_,如果找到了就会使用其创建Foo类;如果没有,会使用type创建这个类。下面这段文字要多读几遍。
当你编写以下代码时:
class Foo(Bar):
pass
Python做了这些事情:
在类Foo中有定义__metaclass__属性吗?
如果有,则继续;
如果没有,Python会在模块层寻找__metaclass__属性(这只针对没有继承任何其他类的情况);
如果模块层也没有,则会在Bar(第一个父类)中寻找(这就有可能是内置的type)。
这样找到__metaclass__后,使用它在内存中创建名称为Foo的类对象(这边跟上,一个类对象)
需要注意的是,__metaclass__属性不会被继承,但是父类的元类(Bar.__class__)可以被继承:如果Bar的__metaclass__属性定义了使用type()(不是type.__new())创建Bar类,其子类不会继承这个行为。(Be careful here that the metaclass attribute will not be inherited, the metaclass of the parent (Bar.__class__) will be. If Bar used a metaclass attribute that created Bar with type() (and not type.__new__()), the subclasses will not inherit that behavior.)TODO 这边不太理解
现在有个新问题,你可以赋什么值给__metaclass__?
答案是:可以创建一个类的东西。
那什么可以创建类?type、子类化type或者使用type的东西。
自定义元类
元类的主要目的,是在创建类的时候动态地改变类。通常你会想创建符合当前上下文的类供API使用。举一个简单的例子,当你希望一个模块中所有的类属性都是小写时,有几种方法可以实现,其中有一种方法就是在模块层设置__metaclass__。使用这种方法,这个模块中所有的类都将使用此元类创建,我们只需要使元类将所有类属性置为小写。
幸运的是,__metaclass__可以被任意调用,不一定非要是一个正式的类(我知道,名称包含“class”不一定非要是一个类,搞清楚了...这很有用)。
现在先用一个函数举一个简单的例子:
# 元类会自动获取通常传给`type`的参数
def upper_attr(future_class_name, future_class_parents, future_class_attr):
"""
返回一个类对象,将其属性置为大写
"""
# 过滤出所有开头不为'__'的属性,置为大写
uppercase_attr = {}
for name, val in future_class_attr.items():
if not name.startswith('__'):
uppercase_attr[name.upper()] = val
else:
uppercase_attr[name] = val
# 利用'type'创建类
return type(future_class_name, future_class_parents, uppercase_attr)
__metaclass__ = upper_attr # 这会影响此模块中所有的类
class Foo(): # global __metaclass__ won't work with "object" though == 没看懂
# but we can define __metaclass__ here instead to affect only this class
# and this will work with "object" children
bar = 'bip'
print(hasattr(Foo, 'bar'))
# Out: False
print(hasattr(Foo, 'BAR'))
# Out: True
f = Foo()
print(f.BAR)
# Out: 'bip'
现在,完成同样的功能,但是为元类定义一个真实的类:
# 记住`type`实际上是一个像`str`和`int`的类,可以用于被继承
class UpperAttrMetaclass(type):
# __new__放在__init__之前调用,此方法创建对象并反回
# 而__init__则是初始化作为参数传递给此方法的对象
# 除非你想控制如何创建一个对象,否则很少用到__new__
# 在这里,被创建的对象是类,而我们想自定义这个类,所以重写了__new__
# 如果需要的话你也可以在__init__中做一些操作
# 一些高级用法会包括重写__call__,不过这里还不需要
def __new__(upperattr_metaclass, future_class_name,
future_class_parents, future_class_attr):
uppercase_attr = {}
for name, val in future_class_attr.items():
if not name.startswith('__'):
uppercase_attr[name.upper()] = val
else:
uppercase_attr[name] = val
return type(future_class_name, future_class_parents, uppercase_attr)
这不是真正的面向对象(OOP),这里直接调用了type,没有重写或者调用父类的__new__。现在像这样处理:
class UpperAttrMetaclass(type):
def __new__(upperattr_metaclass, future_class_name,
future_class_parents, future_class_attr):
uppercase_attr = {}
for name, val in future_class_attr.items():
if not name.startswith('__'):
uppercase_attr[name.upper()] = val
else:
uppercase_attr[name] = val
# 复用type.__new__方法
# 这是基本的OOP,没什么深奥的
return type.__new__(upperattr_metaclass, future_class_name,
future_class_parents, uppercase_attr)
你大概发现了传给type的额外的参数upperattr_metaclass。这没什么奇怪的:__new__的第一个参数总是其定义的类。就像类方法中第一个参数总是self。当然,为了清晰期间,这里我起的名字比较长,但是像self这样的参数通常有一个传统的名字。所以真正的产品代码中,元类是像这样的:
class UpperAttrMetaclass(type):
def __new__(cls, clsname, bases, dct):
uppercase_attr = {}
for name, val in dct.items():
if not name.startswith('__'):
uppercase_attr[name.upper()] = val
else:
uppercase_attr[name] = val
return type.__new__(cls, clsname, bases, uppercase_attr)
使用super可以更清晰,which will ease inheritance (because yes, you can have metaclasses, inheriting from metaclasses, inheriting from type)TODO
:
class UpperAttrMetaclass(type):
def __new__(cls, clsname, bases, dct):
uppercase_attr = {}
for name, val in dct.items():
if not name.startswith('__'):
uppercase_attr[name.upper()] = val
else:
uppercase_attr[name] = val
return super(UpperAttrMetaclass, cls).__new__(cls, clsname, bases, uppercase_attr)
以上,关于元类也没有更多了。使用元类的代码比较复杂的原因不在于元类,而在于你通常会依靠自省、操纵继承、__dict__变量等,使用元类做一些晦涩的事情。(it's because you usually use metaclasses to do twisted stuff relying on introspection, manipulating inheritance, vars such as __dict__, etc.)TODO
元类来用于黑魔法时的确特别有用,因为也会将事情搞得很复杂。但就其本身而言,是简单的:
- 拦截一个类的创建
- 修改类
- 返回修改的类
为什么会用元类代替函数?
既然__metaclass__可以被任意调用,为什么要使用明显更复杂的类呢?有这样一些理由:
- 意图明显。当你看到UpperAttrMetaclass(type),你知道接下来会发生什么。
- 可以使用OOP。元类可以继承自元类,重写父类的方法,元类甚至可以使用元类。
- 如果为一个类指定的元类是类而不是方法,这个类的子类将是元类的一个实例。
Children of a class will be instances of its metaclass if you specified a metaclass-class, but not with a metaclass-function.TODO
- 你可以将代码组织得更好。使用元类时肯定不会仅想像上面举的例子那样简单,通常是用于比较复杂的场景。将多个方法组织在一个类中有益于使代码更容易阅读。
- 你可以使用__new__,__init__ 和 __call__,这些方法可以处理不用的事情。即使很多时候你可以在__new__中完成所有工作,当然,一些人会更习惯用__init__。
- 这些东西叫 “metaclass”,小心了!这一定很难搞!
为什么使用元类
好了,现在的问题是:为什么要是用这样晦涩且容易出错的特性?其实,通常你不会用:
元类是99%的用户根本不必操心的深度魔法。如果你在考虑是否需要使用,那就不要用(真正需要的用户很清楚他们的需求,根本不需要解释为什么要使用元类)
Python领袖 Tim Peters
使用元类的一个主要场景是创建API。Django ORM是一个典型的例子,它允许你像下面这样定义:
class Person(models.Model):
name = models.CharField(max_length=30)
age = models.IntegerField()
如果你这样做:
guy = Person(name='bob', age='35')
print(guy.age)
它不会返回一个IntegerField的对象,而是返回一个整数,甚至可以从数据库中直接获取数据。TODO
这是因为,在models.Model中定义了__metaclass__,使用了一些魔法将你定义的简单的Person类转换为一个复杂的数据库挂钩。(turn the Person you just defined with simple statements into a complex hook to a database field.)TODO
Django使用元类对外提供简单的API,简化了一些复杂的东西,API中重建的代码会去完成幕后真正的工作。
结语
首先,类是可以创建实例的对象。类本身是元类的对象:
>>> class Foo(object): pass
>>> id(Foo)
142630324
在Python中,除了type,一切皆对象,一切都是类或者元类的对象。事实上type是自己的元类,这在纯Python中这是无法实现的,这里在实现层上做了一些手段。
其次,元类是复杂的。你可能不希望对非常简单的类使用元类,那么还有其他两种手段用来改变类:
- monkey patching
- 类装饰器
如果你需要改变类,99%的情况下使用这两种方法。
但其实98%的情况你根本不需要改变类。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。